package com.yanyeori.framework.security.controller;

import com.yanyeori.framework.core.context.UserContext;
import com.yanyeori.framework.core.interfaces.PublishEventUtil;
import com.yanyeori.framework.core.model.WebResponse;
import com.yanyeori.framework.core.util.BeanUtil;
import com.yanyeori.framework.core.util.IdUtil;
import com.yanyeori.framework.core.util.StringUtil;
import com.yanyeori.framework.log.common.annotation.LogIgnore;
import com.yanyeori.framework.log.common.constant.LogIgnoreType;
import com.yanyeori.framework.security.bo.*;
import com.yanyeori.framework.security.constant.SecurityConst;
import com.yanyeori.framework.security.constant.SsoErrorCode;
import com.yanyeori.framework.security.database.UserDao;
import com.yanyeori.framework.security.entity.User;
import com.yanyeori.framework.security.enums.UserStatusEnum;
import com.yanyeori.framework.security.event.LoginEvent;
import com.yanyeori.framework.security.event.RemoteLogoutEvent;
import com.yanyeori.framework.security.service.TokenLoginService;
import com.yanyeori.framework.security.service.UsernamePasswordDecrypt;
import com.yanyeori.framework.security.util.SecurityUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;

/**
 * SSO服务
 *
 * @author chenkuan 2020/11/20
 */
@Slf4j
@RestController
@RequestMapping("/sso")
@LogIgnore(type = {LogIgnoreType.PARAM})
public class SsoController {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private TokenLoginService tokenLoginService;
    @Autowired
    private UserDao userDao;
    @Autowired
    private PublishEventUtil publishEventUtil;

    /**
     * 登录
     */
    @PostMapping("/login")
    public WebResponse<?> login(@RequestBody LoginReqBO loginReqBO, HttpServletRequest request) {
        WebResponse<?> response = validLoginRequest(loginReqBO);
        if (!response.isSuccess()) {
            log.warn(response.getMessage());
            return response;
        }
        String username;
        try {
            username = UsernamePasswordDecrypt.get().username(request, loginReqBO.getUsername());
        } catch (Exception e) {
            log.error("Decrypt username fail", e);
            return WebResponse.fail(SsoErrorCode.SSO_1019);
        }
        log.info("{}, start login authentication...", username);
        //处理登录
        WebResponse<Authentication> loginResponse = tokenLoginService.login(request, loginReqBO.getUsername(), loginReqBO.getPassword());
        if (!loginResponse.isSuccess()) return loginResponse;
        Authentication authentication = loginResponse.getData();
        //获取用户信息
        UserDetail userDetail = (UserDetail) authentication.getPrincipal();
        //生成token
        String token = tokenLoginService.generateToken(userDetail);
        log.debug("Token generated successfully");
        //设备在线维护
        tokenLoginService.deviceOnline(token);
        //发布登陆成功本地通知事件
        loginEvent();
        //返回响应
        log.info("{}, ip: {}, login successful", username, userDetail.getLoginIp());
        return WebResponse.ok(buildLoginResBO(userDetail, token)).message("Login successful");
    }

    /**
     * 登出
     */
    @RequestMapping(value = "/logout", method = {RequestMethod.GET, RequestMethod.POST})
    public WebResponse<?> logout(HttpServletRequest request) {
        String token = SecurityUtil.getToken(request).getData().replaceFirst(SecurityConst.AUTH_BEARER_PRE, "");
        String username = UserContext.username();
        //移除token
        tokenLoginService.removeToken(token);
        //发布远程登出通知事件(每个服务都执行)
        logoutEvent();
        //清除SecurityContext认证信息
        SecurityContextHolder.clearContext();
        //清除用户上下文
        UserContext.clear();
        log.info("{}, log out successful", username);
        return WebResponse.ok().message("Log out successfully");
    }

    /**
     * 注册用户
     */
    @PostMapping("/sign")
    public WebResponse<?> sign(@RequestBody SignReqBO signReqBO, HttpServletRequest request) {
        WebResponse<?> response = validSignUser(signReqBO);
        if (!response.isSuccess()) {
            return response;
        }
        String username;
        String password;
        try {
            username = UsernamePasswordDecrypt.get().username(request, signReqBO.getUsername());
            password = UsernamePasswordDecrypt.get().password(request, signReqBO.getPassword());
        } catch (Exception e) {
            log.error("Decrypt username or password fail", e);
            return WebResponse.fail(SsoErrorCode.SSO_1019);
        }
        //校验用户是否已存在
        User tempUser = userDao.getByUsername(username);
        if (tempUser != null) {
            //用户名已存在
            return WebResponse.fail(SsoErrorCode.SSO_1005);
        }
        //插入用户
        User user = buildSignUser(username, password, signReqBO.getNickname());
        int count = userDao.insert(user);
        if (count == 1) {
            log.info("User [{}] registered successfully", user.getUsername());
            return WebResponse.ok().message("Registered successfully");
        } else {
            log.error("User [{}] registered failed", user.getUsername());
            //注册失败
            return WebResponse.fail(SsoErrorCode.SSO_1017);
        }
    }

    /**
     * 获取用户信息
     */
    @RequestMapping(value = "/user", method = {RequestMethod.GET, RequestMethod.POST})
    public WebResponse<UserDetail> getUser() {
        UserDetail userDetail = (UserDetail) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return WebResponse.ok(userDetail);
    }

    /**
     * 修改密码
     */
    @PostMapping("/changePwd")
    public WebResponse<?> changePwd(@RequestBody @Valid PwdChangeReqBO pwdChangeReqBO, HttpServletRequest request) {
        String password;
        String newPassword;
        try {
            password = UsernamePasswordDecrypt.get().password(request, pwdChangeReqBO.getPassword());
            newPassword = UsernamePasswordDecrypt.get().password(request, pwdChangeReqBO.getNewPassword());
        } catch (Exception e) {
            log.error("Decrypt password fail", e);
            return WebResponse.fail(SsoErrorCode.SSO_1019);
        }
        //校验原密码是否正确
        User user = getUser().getData();
        UserDetail userDetail = userDao.getByUsername(user.getUsername());
        if (!passwordEncoder.matches(password, userDetail.getPassword())) {
            //原密码不正确
            return WebResponse.fail(SsoErrorCode.SSO_1014);
        }
        //校验原密码和新密码是否一样
        if (password.equals(newPassword)) {
            //新密码与原密码不能相同
            return WebResponse.fail(SsoErrorCode.SSO_1015);
        }
        //更新密码
        long time = System.currentTimeMillis();
        user.setPwdUpdateTime(time);
        //密码错误次数重置为0
        user.setPwdErrorTimes(0);
        user.setUpdateTime(time);
        user.setUpdateUser(user.getUsername());
        int count = userDao.updatePwd(user, passwordEncoder.encode(newPassword));
        if (count == 1) {
            log.info("User [{}] password modified successfully", user.getUsername());
            //密码修改成功后登出用户
            logout(request);
            return WebResponse.ok().message("Password modified successfully");
        } else {
            log.error("User [{}] password modified failed", user.getUsername());
            //密码修改失败
            return WebResponse.fail(SsoErrorCode.SSO_1016);
        }
    }

    /**
     * 登录，发布本地登录事件
     */
    private void loginEvent() {
        publishEventUtil.publishEvent(BeanUtil.copyProperties(UserContext.getRequired(), LoginEvent.class), false);
        log.debug("Publish login event: LoginEvent");
    }

    /**
     * 登出，发布远程登出事件
     */
    private void logoutEvent() {
        //发布远程登出通知事件(每个服务都执行)
        publishEventUtil.publishEvent(BeanUtil.copyProperties(UserContext.getRequired(), RemoteLogoutEvent.class), true);
        log.debug("Publish remote logout event: RemoteLogoutEvent");
    }

    /**
     * 校验登录请求信息
     */
    private WebResponse<?> validLoginRequest(LoginReqBO loginReqBO) {
        if (StringUtil.isBlank(loginReqBO.getUsername())) {
            //用户名不能为空
            return WebResponse.fail(SsoErrorCode.SSO_1006);
        }
        if (StringUtil.isBlank(loginReqBO.getPassword())) {
            //密码不能为空
            return WebResponse.fail(SsoErrorCode.SSO_1007);
        }
        return WebResponse.ok();
    }

    /**
     * 校验注册用户信息
     */
    private WebResponse<?> validSignUser(SignReqBO signReqBO) {
        if (StringUtil.isBlank(signReqBO.getUsername())) {
            //用户名不能为空
            return WebResponse.fail(SsoErrorCode.SSO_1006);
        }
        if (StringUtil.isBlank(signReqBO.getPassword())) {
            //密码不能为空
            return WebResponse.fail(SsoErrorCode.SSO_1007);
        }
        if (StringUtil.isBlank(signReqBO.getNickname())) {
            //用户昵称不能为空
            return WebResponse.fail(SsoErrorCode.SSO_1008);
        }
        return WebResponse.ok();
    }

    /**
     * 根据注册信息构建用户
     */
    private User buildSignUser(String username, String password, String nickname) {
        User user = new User();
        user.setUserId(IdUtil.simpleUUID());
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(password));
        user.setNickname(nickname);
        user.setPwdUpdateTime(System.currentTimeMillis());
        user.setPwdErrorTimes(0);
        user.setStatus(UserStatusEnum.ACTIVE.getCode());
        user.setCreateUser(user.getUsername());
        user.setCreateTime(System.currentTimeMillis());
        user.setUpdateUser(user.getUsername());
        user.setUpdateTime(System.currentTimeMillis());
        return user;
    }

    /**
     * 登录成功后构建登录响应
     */
    private LoginResBO buildLoginResBO(UserDetail userDetail, String token) {
        return new LoginResBO(userDetail, token, System.currentTimeMillis());
    }
}
